/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.impl.bpmn.behavior;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.HistoricActivityInstanceQueryImpl;
import org.activiti.engine.impl.bpmn.helper.ScopeUtil;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.HistoricActivityInstanceEntity;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.runtime.InterpretableExecution;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.EventDefinition;
import org.flowable.bpmn.model.TerminateEventDefinition;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
import org.flowable.common.engine.impl.history.HistoryLevel;
import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants;
import org.flowable.engine.delegate.DelegateExecution;

/**
 * @author Martin Grofcik
 * @author Tijs Rademakers
 * @author Joram Barrez
 */
public class TerminateEndEventActivityBehavior extends FlowNodeActivityBehavior {

    private static final long serialVersionUID = 1L;

    protected EndEvent endEvent;
    protected boolean terminateAll;

    public TerminateEndEventActivityBehavior(EndEvent endEvent) {
        this.endEvent = endEvent.clone();

        // Terminate all attribute
        if (endEvent.getEventDefinitions() != null) {
            for (EventDefinition eventDefinition : endEvent.getEventDefinitions()) {
                if (eventDefinition instanceof TerminateEventDefinition) {
                    TerminateEventDefinition terminateEventDefinition = (TerminateEventDefinition) eventDefinition;
                    if (terminateEventDefinition.isTerminateAll()) {
                        this.terminateAll = true;
                        break;
                    }
                }
            }
        }

    }

    @Override
    public void execute(DelegateExecution delegateExecution) {
        ActivityExecution execution = (ActivityExecution) delegateExecution;
        ActivityImpl terminateEndEventActivity = (ActivityImpl) execution.getActivity();

        if (terminateAll) {
            ActivityExecution processInstanceExecution = findRootProcessInstanceExecution((ExecutionEntity) execution);
            terminateProcessInstanceExecution(execution, terminateEndEventActivity, processInstanceExecution);
        } else {
            ActivityExecution scopeExecution = ScopeUtil.findScopeExecution(execution);
            if (scopeExecution != null) {
                terminateExecution(execution, terminateEndEventActivity, scopeExecution);
            }
        }

    }

    /**
     * Finds the parent execution that is a process instance. For a callactivity, this will be the process instance representing the called process instance and NOT the root process instance!
     */
    protected ActivityExecution findProcessInstanceExecution(ActivityExecution execution) {
        ActivityExecution currentExecution = execution;
        while (currentExecution.getParent() != null) {
            currentExecution = currentExecution.getParent();
        }
        return currentExecution;
    }

    protected ActivityExecution findRootProcessInstanceExecution(ExecutionEntity execution) {
        ExecutionEntity currentExecution = execution;
        while (currentExecution.getParentId() != null || currentExecution.getSuperExecutionId() != null) {
            ExecutionEntity parentExecution = currentExecution.getParent();
            if (parentExecution != null) {
                currentExecution = parentExecution;
            } else if (currentExecution.getSuperExecutionId() != null) {
                currentExecution = currentExecution.getSuperExecution();
            }
        }
        return currentExecution;
    }

    protected void terminateExecution(ActivityExecution execution, ActivityImpl terminateEndEventActivity, ActivityExecution scopeExecution) {
        // send cancelled event
        sendCancelledEvent(execution, terminateEndEventActivity, scopeExecution);

        // destroy the scope
        scopeExecution.destroyScope("terminate end event fired");

        // set the scope execution to the terminate end event and make it end here.
        // (the history should reflect that the execution ended here and we want an 'end time' for the
        // historic activity instance.)
        ((InterpretableExecution) scopeExecution).setActivity(terminateEndEventActivity);
        // end the scope execution
        scopeExecution.end();

        // Scope execution can already have been ended (for example when multiple seq flow arrive in the same terminate end event)
        // in that case, we need to make sure the activity instance is ended
        if (scopeExecution.isEnded()) {
            Context.getCommandContext().getHistoryManager().recordActivityEnd((ExecutionEntity) execution);
        }

    }

    protected void terminateProcessInstanceExecution(ActivityExecution execution, ActivityImpl terminateEndEventActivity, ActivityExecution processInstanceExecution) {
        sendCancelledEvent(execution, terminateEndEventActivity, processInstanceExecution);
        deleteProcessInstance((ExecutionEntity) processInstanceExecution, execution, "terminate end event (" + terminateEndEventActivity.getId() + ")");
    }

    protected void deleteProcessInstance(ExecutionEntity processInstanceExecution, ActivityExecution execution, String deleteReason) {

        List<ExecutionEntity> orderedExecutions = orderExecutionsRootToLeaf(processInstanceExecution);
        Collections.reverse(orderedExecutions);

        endAllHistoricActivities(processInstanceExecution.getId());

        for (ExecutionEntity executionToDelete : orderedExecutions) {

            executionToDelete.setDeleteReason(deleteReason);
            executionToDelete.setEnded(true);
            executionToDelete.setActive(false);
            executionToDelete.setDeleteRoot(true);

            executionToDelete.remove();
        }

        Context.getCommandContext().getHistoryManager().recordProcessInstanceEnd(processInstanceExecution.getId(), deleteReason, execution.getActivity().getId());
    }

    protected List<ExecutionEntity> orderExecutionsRootToLeaf(ExecutionEntity execution) {

        // Find root process instance
        ExecutionEntity rootExecution = execution;
        while (rootExecution.getParent() != null || rootExecution.getSuperExecution() != null) {
            rootExecution = rootExecution.getParent() != null ? rootExecution.getParent() : rootExecution.getSuperExecution();
        }

        return orderExecutionsRootToLeaf(rootExecution, new ArrayList<>());
    }

    protected List<ExecutionEntity> orderExecutionsRootToLeaf(ExecutionEntity rootExecution, List<ExecutionEntity> orderedExecutions) {
        orderedExecutions.add(rootExecution);

        // Children
        if (rootExecution.getExecutions() != null && rootExecution.getExecutions().size() > 0) {
            for (ExecutionEntity childExecution : rootExecution.getExecutions()) {
                orderExecutionsRootToLeaf(childExecution, orderedExecutions);
            }
        }

        // Called process instances (subprocess)
        if (rootExecution.getSubProcessInstance() != null) {
            orderExecutionsRootToLeaf(rootExecution.getSubProcessInstance(), orderedExecutions);
        }

        return orderedExecutions;
    }

    protected void endAllHistoricActivities(String processInstanceId) {

        if (!Context.getProcessEngineConfiguration().getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) {
            return;
        }

        Map<String, HistoricActivityInstanceEntity> historicActivityInstancMap = new HashMap<>();

        List<HistoricActivityInstance> historicActivityInstances = new HistoricActivityInstanceQueryImpl(Context.getCommandContext())
                .processInstanceId(processInstanceId)
                .unfinished()
                .list();
        for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) {
            historicActivityInstancMap.put(historicActivityInstance.getId(), (HistoricActivityInstanceEntity) historicActivityInstance);
        }

        // Cached version overwrites entity
        List<HistoricActivityInstanceEntity> cachedHistoricActivityInstances = Context.getCommandContext().getDbSqlSession()
                .findInCache(HistoricActivityInstanceEntity.class);
        for (HistoricActivityInstanceEntity cachedHistoricActivityInstance : cachedHistoricActivityInstances) {
            if (processInstanceId.equals(cachedHistoricActivityInstance.getProcessInstanceId())
                    && (cachedHistoricActivityInstance.getEndTime() == null)) {
                historicActivityInstancMap.put(cachedHistoricActivityInstance.getId(), cachedHistoricActivityInstance);
            }
        }

        for (HistoricActivityInstanceEntity historicActivityInstance : historicActivityInstancMap.values()) {
            historicActivityInstance.markEnded(null);

            // Fire event
            ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
            if (config != null && config.getEventDispatcher().isEnabled()) {
                config.getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_ACTIVITY_INSTANCE_ENDED, historicActivityInstance),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }
        }

    }

    protected void sendCancelledEvent(ActivityExecution execution, ActivityImpl terminateEndEventActivity, ActivityExecution scopeExecution) {
        if (Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) {
            Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                    ActivitiEventBuilder.createCancelledEvent(execution.getId(), execution.getProcessInstanceId(),
                            execution.getProcessDefinitionId(), terminateEndEventActivity),
                    EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
        }
        dispatchExecutionCancelled(scopeExecution, terminateEndEventActivity);
    }

    private void dispatchExecutionCancelled(ActivityExecution execution, ActivityImpl causeActivity) {
        // subprocesses
        for (ActivityExecution subExecution : execution.getExecutions()) {
            dispatchExecutionCancelled(subExecution, causeActivity);
        }

        // call activities
        ExecutionEntity subProcessInstance = Context.getCommandContext().getExecutionEntityManager().findSubProcessInstanceBySuperExecutionId(execution.getId());
        if (subProcessInstance != null) {
            dispatchExecutionCancelled(subProcessInstance, causeActivity);
        }

        // activity with message/signal boundary events
        ActivityImpl activity = (ActivityImpl) execution.getActivity();
        if (activity != null && activity.getActivityBehavior() != null && activity != causeActivity) {
            dispatchActivityCancelled(execution, activity, causeActivity);
        }
    }

    protected void dispatchActivityCancelled(ActivityExecution execution, ActivityImpl activity, ActivityImpl causeActivity) {
        Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                ActivitiEventBuilder.createActivityCancelledEvent(activity.getId(),
                        (String) activity.getProperties().get("name"),
                        execution.getId(),
                        execution.getProcessInstanceId(), execution.getProcessDefinitionId(),
                        (String) activity.getProperties().get("type"),
                        activity.getActivityBehavior().getClass().getCanonicalName(),
                        causeActivity),
                EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
    }

    public EndEvent getEndEvent() {
        return this.endEvent;
    }

}
